話是這麼說,但要明白即使我們將密碼加密儲存了,但在使用過程中依舊會有暴露的風險。因此還是會建議在使用這個App的時候不要使用本尊帳號(這也是我沒做推文功能的原因之一),並且定期更換密碼,除此之外平時手機的使用習慣還是要謹慎為上!
Android keystore system是Android中用來儲存加密金鑰的系統,自Android 7.0起,在Android Compatibility Definition Document中已將幾種演算法的金鑰儲存至Hardware Backed Keystore訂為MUST have:
MUST have hardware backed implementations of RSA, AES, ECDSA and HMAC cryptographic algorithms and MD5, SHA1, SHA-2 Family hash functions to properly support the Android Keystore system's supported algorithms.
因此以目前來說使用Keystore System來儲存我們的金鑰是相對安全的做法。
以我們的使用情境來說,需要加密的資料只有密碼,因此資料量不大。在這情況下我是選擇以安全性為主,因此直接使用RSA演算法做加解密。
以下產生金鑰方法為API 23以上的做法,兼容低版本的部分可參考文末的參考文章。
class KeystoreUtil {
private val keyStoreProvider = "AndroidKeyStore"
private val alias = "ALIAS_CA"
private val keystore: KeyStore = KeyStore.getInstance(keyStoreProvider)
init {
keystore.load(null)
if (!keystore.containsAlias(alias)) {
genRSAKey()
}
}
private fun genRSAKey() {
val keyPairGenerator =
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, keyStoreProvider)
val keyGenParameterSpec = KeyGenParameterSpec
.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
.build()
keyPairGenerator.initialize(keyGenParameterSpec)
keyPairGenerator.generateKeyPair()
}
// ...
}
class KeystoreUtil {
// ...
public fun encrypt(plaintext: String): String {
val publicKey = keystore.getCertificate(alias).publicKey
val cipher = Cipher.getInstance(rsaMode)
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
return cipher.doFinal(plaintext.toByteArray()).toHexString()
}
// ...
}
toHexString是另外做的擴展函數,將加密後的ByteArray轉為Hex字串:
fun ByteArray.toHexString(): String =
joinToString(separator = "") { byte ->
"%02x".format(byte)
}
class KeystoreUtil {
// ...
public fun decrypt(encryptedText: String): String {
val privateKey = keystore.getKey(alias, null) as PrivateKey
val cipher = Cipher.getInstance(rsaMode)
cipher.init(Cipher.DECRYPT_MODE, privateKey)
return cipher.doFinal(encryptedText.hexToByteArray()).toString(StandardCharsets.UTF_8)
}
// ...
}
hexToByteArray則是對應的將Hex字串轉回ByteArray的方法:
fun String.hexToByteArray(): ByteArray =
chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
另外注意解密回來後的ByteArray在轉回String時有帶入StandardCharsets.UTF_8,這是因為Kotlin String預設的toByteArray中有預設指定的Charset Type。
public inline fun String.toByteArray(charset: Charset = Charsets.UTF_8): ByteArray =
(this as java.lang.String).getBytes(charset)
KeystoreUtil完成後就可以直接把昨天存取密碼的位置做更換了。
class LoginFragment : Fragment() {
// ...
private val keystoreUtil = KeystoreUtil()
// ...
}
// ...
if (binding.savePwd.isChecked) {
editor.putString(PREF_FIELD_PWD, keystoreUtil.encrypt(pwd))
}
// ...
// ...
val encryptedPwd = preferences.getString(PREF_FIELD_PWD, null)
if (!encryptedPwd.isNullOrBlank()) {
binding.pwdInput.setText(keystoreUtil.decrypt(encryptedPwd))
binding.savePwd.isChecked = true
}
// ...
以下文章使用非對稱式演算法加密金鑰、對稱式演算法加密內文的方法是較為兼顧效能與安全性的方法。
使用Android KeyStore 儲存敏感性資料